﻿using System;
using System.Linq;
using System.Threading.Tasks;
using log4net;
using SDCloudSolution.SDCloud.BusinessLayer.Entities;
using SDCloudSolution.SDCloud.BusinessLayer.Interfaces;
using SDCloudSolution.SDCloud.BusinessLayer.Model.Interfaces;
using SDCloudSolution.SDCloud.BusinessLayer.ModelData.Interfaces;
using SDCloudSolution.SDCloud.Entities.Models;
using SDCloudSolution.SDCloud.Entities.Models.Data;
using SDCloudSolution.SDCloud.ModelRunAgent.Infrastructure.Interfaces;
using SDCloudSolution.SDCloud.ServiceAgent;

namespace SDCloudSolution.SDCloud.ModelRunAgent.Infrastructure
{
    public class ModelExecutorComponent : IModelExecutorComponent
    {
        private readonly IModelRunComponent _modelRunComponent;
        private readonly IModelComponent _modelComponent;
        private readonly IModelExecutorFactory _modelExecutorFactory;
        private readonly IModelExecutionResultComponent _modelExecutionResultComponent;
        private readonly IModelStatisticsComponent _modelStatisticsComponent;
        private readonly IModelFileStorageProvider _modelFileStorageProvider;
        private readonly ILog _logger;

        public ModelExecutorComponent(
            IModelRunComponent modelRunComponent, 
            IModelComponent modelComponent, 
            IModelExecutorFactory pythonEngineExecutorFactory,
            IModelExecutionResultComponent modelExecutionResultComponent,
            IModelStatisticsComponent modelStatisticsComponent,
            IModelFileStorageProvider modelFileStorageProvider, 
            ILog logger)
        {
            _modelRunComponent = modelRunComponent;
            _modelComponent = modelComponent;
            _modelExecutorFactory = pythonEngineExecutorFactory;
            _modelExecutionResultComponent = modelExecutionResultComponent;
            _modelStatisticsComponent = modelStatisticsComponent;
            _modelFileStorageProvider = modelFileStorageProvider;
            _logger = logger;
        }

        public async Task ExecuteModel(ModelRunEntity modelRunEntity)
        {
            var modelEntity = await _modelComponent.GetModelByIdAsync(modelRunEntity.ModelId);
            var modelFullFilePath = _modelFileStorageProvider.GetModelFullFilePath(modelEntity);

            var context = new ModelExecutionContextProvider(_modelRunComponent, modelRunEntity);

            try
            {
                var modelExecutor = _modelExecutorFactory.CreateModelExecutor(context);

                var timeStart = DateTime.UtcNow;

                var result = await modelExecutor.ExecuteModel(modelFullFilePath);

                var metadata = await SaveModelExecutionData(modelRunEntity, result);
                var timeEnd = DateTime.UtcNow;

                var executionTime = (long) Math.Ceiling((timeEnd - timeStart).TotalMilliseconds);

                var metrics = new ModelExecutionMetrics
                {
                    ModelId = modelRunEntity.ModelId,
                    TimeExecutionInMilliseconds = executionTime,
                    UsedDataSizeInBytes = metadata.ResultSizeInBytes
                };

                var saveMetricsTask = _modelStatisticsComponent.LogSuccessModelExecutionMetricsAsync(metrics);

                context.CurrentModelRunEntity.EndDateUtc = timeEnd;
                context.CurrentModelRunEntity.Status = ModelRunStatus.Success;
                var completeModelExecutionTask = _modelRunComponent.UpdateModelRunAsync(context.CurrentModelRunEntity);

                Task.WaitAll(saveMetricsTask, completeModelExecutionTask);
            }
            catch (Exception exception)
            {
                _logger.Error($"Can't execute the model [{modelRunEntity.ModelId}]", exception);

                // We have an error => need to store it
                var saveMetricsTask = _modelStatisticsComponent.LogFailedModelExecutionAsync(modelRunEntity.ModelId);

                context.CurrentModelRunEntity.EndDateUtc = DateTime.UtcNow;
                context.CurrentModelRunEntity.Status = ModelRunStatus.Error;
                var completeExecutionTask = _modelRunComponent.UpdateModelRunAsync(context.CurrentModelRunEntity);

                Task.WaitAll(saveMetricsTask, completeExecutionTask);
            }
        }

        private async Task<ModelExecutionMetadataEntity> SaveModelExecutionData(ModelRunEntity modelRunEntity, ModelResultDataEntity result)
        {
            result.Metadata = new ModelExecutionMetadataEntity
            {
                ModelId = modelRunEntity.ModelId,
                ModelRunId = modelRunEntity.Id,
                CreationDateUtc = DateTime.UtcNow,
                ResultSizeInBytes = ComputeDataSizeInBytes(result)
            };

            foreach (var timeFrame in result.TimeFrames)
            {
                timeFrame.ModelId = modelRunEntity.ModelId;
                timeFrame.ModelExecutionId = modelRunEntity.Id;
            }

            await _modelExecutionResultComponent.InsertExecutionResutls(result);

            return result.Metadata;
        }

        private long ComputeDataSizeInBytes(ModelResultDataEntity entity)
        {
            return entity.Columns.Sum(column => (column.Name?.Length ?? 0 + column?.DisplayName.Length ?? 0 + column.UnitName?.Length ?? 0) * sizeof(char)) +
                   entity.TimeFrames.Sum(frame => frame.Values.Count * sizeof(double));
        }
    }
}
